Перейти к основному содержимому

3.06. Справочник по Cypher

Разработчику Аналитику Тестировщику
Архитектору Инженеру

Справочник по Cypher

1. Общая структура запроса

Cypher — декларативный язык запросов, вдохновлённый SQL и ASCII-графикой. Запрос строится из клауз, исполняемых в строго определённом порядке:

[USE database]
[MATCH ...]
[OPTIONAL MATCH ...]
[WHERE ...]
[WITH ...]
[UNWIND ...]
[CREATE ... | MERGE ...]
[SET ... | REMOVE ...]
[DELETE ... | DETACH DELETE ...]
[RETURN ...]
[ORDER BY ...]
[SKIP ...]
[LIMIT ...]

Клаузы не могут следовать в произвольном порядке. Допустимые порядки определяются синтаксической грамматикой Cypher (ISO/IEC 39075:2022 — GQL, в котором Cypher лег в основу).

Фазы выполнения запроса

  1. Определение контекста: USE, CALL { ... }, подзапросы.
  2. Чтение данных: MATCH, OPTIONAL MATCH.
  3. Фильтрация: WHERE.
  4. Группировка данных: WITH, UNWIND.
  5. Мутации: CREATE, MERGE, SET, REMOVE, DELETE.
  6. Проекция результата: RETURN, ORDER BY, SKIP, LIMIT.

⚠️ Обратите внимание: в Cypher нет поддержки транзакций на уровне синтаксиса в виде BEGIN/COMMIT. Транзакции управляются на уровне протокола (Bolt) или через :auto в cypher-shell, либо явно в драйверах.


2. Сущности графа и их свойства

2.1 Узлы (Nodes)

Синтаксис:

(n)               — неназванный узел
(n:Label) — узел с меткой
(n:Label1:Label2) — узел с несколькими метками
(n {prop: value}) — узел со свойствами

Свойства узла:

  • Любое количество пар «ключ–значение».
  • Ключ — строка, идентификатор (например, name, created_at).
  • Значение — литерал одного из поддерживаемых типов (см. раздел 4).
  • Свойства неиндексируются по умолчанию. Индекс или ограничение (CREATE INDEX, CREATE CONSTRAINT) создаётся явно и применяется только к свойствам конкретных меток.

Особенности:

  • Узел может иметь ноль, одну или множество меток.
  • Метки — это не типы, а теги; не гарантируют схемы, но позволяют фильтровать (MATCH (n:User)).
  • Нельзя создать узел без меток через CREATE — по крайней мере одна метка обязательна в Neo4j 5+ (в более ранних версиях допускались «анонимные» узлы, но они не рекомендовались).

2.2 Рёбра (Relationships)

Синтаксис:

-[r]-               — ненаправленное, без типа
-[r:TYPE]- — ненаправленное, с типом
-[r:TYPE]-> — направленное (от узла слева к узлу справа)
<-[r:TYPE]- — направленное (в обратную сторону)
-[r {prop: v}]-> — с свойствами

Свойства ребра:

  • Аналогично узлам: пары ключ–значение.
  • Каждое ребро обязательно имеет ровно один тип (TYPE).
  • Тип ребра не может быть параметром (нельзя писать -[r:$type]->).
  • Ребро не может существовать без двух узлов-концов.

Особенности:

  • Направление — логическая конструкция. Физически в Neo4j хранятся оба указателя (from→to, to←from), но семантика направления определяется в запросе.
  • В Neo4j рёбра не могут иметь меток (в отличие, например, от ArangoDB).
  • Нельзя создать ребро между одинаковыми узлами (саморебро, self-loop) без явного указания — но саморёбра разрешены и поддерживаются.

2.3 Паттерны (Patterns)

Цепочка узлов и рёбер в MATCH называется паттерном. Примеры:

(a)-[r]->(b)                 — одно ребро
(a)-[r1]->(b)<-[r2]-(c) — вилка
(a)-[*1..3]->(b) — переменная длина (от 1 до 3 рёбер)
(a)-[*]->(b) — любая длина ≥1 (осторожно: может быть очень тяжёлым)
(a)-[r:KNOWS*2]->(b) — ровно 2 ребра типа KNOWS

Сокращённые формы:

  • --> эквивалентно -[]->
  • ()-->()-->() — цепочка из трёх узлов и двух рёбер без именования

Для переменной длины:

  • *n — ровно n рёбер
  • *n..m — от n до m (включительно)
  • *n.. — ≥n
  • *..m — ≤m
  • * — ≥1 (синоним *1..)

⚠️ Переменная длина без ограничений (*) ведёт к экспоненциальному росту пути и крайне не рекомендуется в production без LIMIT, WHERE на свойствах или алгоритмических ограничений (например, shortestPath).


3. Клаузы и их параметры

3.1 MATCH

  • Выполняет поиск по существующим данным.
  • Если паттерн не найден — строка выпадает из потока (как INNER JOIN).
  • Поддерживает:
    • Именованные и анонимные узлы/рёбра.
    • Переменную длину.
    • Неоднородные типы: (a)-[:FRIENDS|COLLEAGUE]->(b)
    • Ограничения в WHERE, а не в самом паттерне (паттерн описывает топологию, фильтрация — отдельно).

Пример:

MATCH (u:User)-[r:SENT]->(m:Message)-[:TO]->(v:User)
WHERE u.active = true AND m.timestamp > datetime('2025-01-01')
RETURN u.name, v.name, count(m) AS messages

3.2 OPTIONAL MATCH

  • Аналог LEFT JOIN.
  • Если паттерн не найден — подставляются null для всех связанных переменных.
  • Может быть несколько OPTIONAL MATCH подряд.

3.3 WHERE

  • Применяется после MATCH, до WITH/RETURN.
  • Поддерживает логические операторы: AND, OR, XOR, NOT.
  • Сравнения: =, <>, <, <=, >, >=, IS NULL, IS NOT NULL.
  • Строковые операторы: STARTS WITH, ENDS WITH, CONTAINS.
  • Регулярные выражения: =~ 'regexp' (Java-совместимый синтаксис).
  • Проверка вхождения: prop IN list, value IN [...], label IN labels(node).
  • Составные условия: WHERE (n.age > 18 AND n.country = 'RU') OR (n.is_verified = true).

3.4 WITH

  • Преобразует промежуточный поток строк.
  • Позволяет:
    • Агрегировать (count, sum, collect и др.).
    • Фильтровать (WHERE после WITH).
    • Переименовывать переменные.
    • Передавать только часть данных в следующую фазу (экономия памяти).

🔹 Важно: после WITH недоступны ранее объявленные переменные, если они не перечислены явно.

Пример:

MATCH (u:User)-[:POSTED]->(p:Post)
WITH u, count(p) AS post_count
WHERE post_count > 10
RETURN u.name, post_count
ORDER BY post_count DESC

3.5 UNWIND

  • Разворачивает список в строки.
  • Обратная операция к collect().
UNWIND [1, 2, 3] AS x
RETURN x * 2 // → 2, 4, 6

Часто используется для импорта массивов свойств или подготовки данных для MERGE.

3.6 CREATE

  • Создаёт новые узлы и рёбра.
  • Не проверяет дубликаты — каждый вызов CREATE порождает новые сущности.
  • Рекомендуется использовать только при заведомо уникальных объектах.

3.7 MERGE

  • Поиск + условное создание.

  • Синтаксис:

    MERGE (n:Label {key: value})
    ON CREATE SET n.created = timestamp()
    ON MATCH SET n.last_seen = timestamp()
  • Проверяет весь паттерн целиком, а не по частям.

  • Может привести к созданию «фантомных» узлов при некорректной спецификации условий.

⚠️ MERGE на паттерне (a)-[r]->(b) без привязки к существующим узлам может создать два новых узла и ребро — даже если a и b должны были быть известны. Рекомендуется сначала MATCH, затем MERGE с привязкой.

3.8 SET

  • Назначает или обновляет свойства.
  • Способы:
    SET n.prop = value
    SET n += {prop1: v1, prop2: v2} — объединение (merge, не полная замена)
    SET n:Label — добавить метку
    SET n:Label1:Label2 — несколько меток

🔸 SET n = {...} полностью заменяет все свойства — будьте осторожны.

3.9 REMOVE

  • Удаляет свойства или метки:
    REMOVE n.prop
    REMOVE n:Label

3.10 DELETE и DETACH DELETE

  • DELETE r — удаляет только ребро.
  • DELETE nошибка, если у узла остались рёбра (ограничение целостности).
  • DETACH DELETE n — удаляет узел и все инцидентные рёбра.

4. Типы данных в Cypher

Cypher поддерживает следующие скалярные и составные типы (в Neo4j 5.x):

ТипПримерОписание
NullnullОтсутствие значения.
Booleantrue, falseЛогический тип.
Integer42, -100064-битное целое (long).
Float3.14, -0.001e564-битное число с плавающей точкой (double).
String"hello"UTF-8, неизменяемая строка.
Datedate('2025-11-21')Календарная дата (без времени).
Timetime('14:30:00+03:00')Время с временной зоной.
LocalTimelocaltime('14:30:00')Время без зоны.
DateTimedatetime('2025-11-21T14:30:00+03:00')Дата + время + зона.
LocalDateTimelocaldatetime('2025-11-21T14:30:00')Дата + время, без зоны.
Durationduration('P1Y2M3DT4H5M6.789S')ISO 8601-совместимый интервал.
Pointpoint({x: 1.0, y: 2.0}), point({latitude: 51.5, longitude: -0.12})2D/3D точка; поддержка декартовых и географических координат.

Составные типы:

  • List — упорядоченный массив значений: [1, "a", true]
    • Гетерогенный (разные типы допустимы).
    • Индексация: list[0], list[-1], срезы list[1..3].
  • Map — именованный набор пар: {name: "Alice", age: 30}
    • Ключи — всегда строки, значения — любые типы.
    • Доступ: map.name, map['name'].

❗ Нет встроенных типов: byte, short, BigDecimal, UUID, enum. Для UUID обычно используется строка ("550e8400-e29b-41d4-a716-446655440000").


5. Встроенные функции Cypher

Функции делятся на категории по назначению и поведению. Все функции чистые (не имеют побочных эффектов), за исключением генераторов случайных чисел и времён (rand(), timestamp()), которые считаются нестабильными.

5.1 Типизация и проверка

ФункцияСигнатураОписание
coalesce(a, b, …)coalesce(value1, value2, …) → valueВозвращает первый ненулевой аргумент. Аналог SQL COALESCE.
exists(pattern)exists((n)-[:R]->())Возвращает true, если заданный паттерн существует. Работает только в WHERE. Устаревшая (deprecated в Neo4j 5+), заменена на IS NOT NULL или COUNT { … } > 0.
type(r)type(relationship) → StringВозвращает тип ребра как строку.
startNode(r), endNode(r)startNode(r) → NodeВозвращает соответственно начальный и конечный узел ребра.
id(n), id(r)id(entity) → IntegerВозвращает внутренний идентификатор Neo4j (не стабилен, не рекомендуется к использованию в бизнес-логике).
labels(n)labels(node) → List<String>Возвращает все метки узла в виде списка. Порядок не гарантирован.
keys(map_or_node)keys({a:1, b:2}) → ['a','b']Возвращает список ключей (имён свойств).

5.2 Скалярные функции

ФункцияОписание
size(list | string | pattern)Длина списка ([1,2,3] → 3), строки ("abc" → 3) или кол-во рёбер в пути (size((a)-[*]->(b))). Для путей с переменной длиной — неэффективна.
length(path)Аналог size(path), но только для путей. MATCH p = (a)-[*]->(b) RETURN length(p)
timestamp()Текущее время в миллисекундах с Unix-эпохи (1732212000000). Неустойчивая функция.
rand()Случайное число [0.0, 1.0). Неустойчивая.
toBoolean(x)Преобразует в Boolean: строки "true"/"false" (регистронезависимо), 1/0, true/false. Иначе — null.
toInteger(x), toFloat(x), toString(x)Строгие преобразования. Ошибки → null.
datetime(x), date(x), time(x) и аналогиКонструкторы временных типов из строк, epoch-миллисекунд, map’ов. Пример: datetime({year:2025, month:11, day:21, hour:15})

5.3 Строковые функции

ФункцияПоведение
trim(str), ltrim(str), rtrim(str)Удаление пробелов (Unicode whitespace).
toLower(str), toUpper(str)Регистронезависимо (Unicode-aware).
substring(str, start [, length])substring("abcdef", 1, 3) → "bcd"
replace(str, old, new)Замена всех вхождений.
split(str, delimiter)Возвращает список. split("a,b,c", ",") → ["a","b","c"]
reverse(str)reverse("abc") → "cba"
left(str, n), right(str, n)Первые/последние n символов.
toStringOrNull(x)Безопасное преобразование: возвращает null при невозможности, не падает.

5.4 Списковые функции

ФункцияПример
range(start, end [, step])range(1, 5) → [1,2,3,4,5]; range(0, 10, 3) → [0,3,6,9]
head(list), last(list)Первый/последний элемент. При пустом списке — null.
tail(list)Все элементы, кроме первого. tail([1,2,3]) → [2,3]
size(list)Уже описано выше.
reverse(list)Обратный порядок.
extract(item IN list | expr)Устаревшая. Заменена на списковые comprehensions: [item IN list | expr]
filter(item IN list WHERE pred)[item IN list WHERE item > 0]
[x IN list WHERE x > 0 | x * 2]Комплексное выражение: фильтрация + преобразование.

🔹 Важно: Cypher поддерживает list comprehensions и pattern comprehensions — мощные инструменты для inline-обработки коллекций.

Пример pattern comprehension:

MATCH (u:User)
RETURN u.name, [ (u)-[:FRIENDS]->(f) | f.name ] AS friends

Возвращает имя пользователя и список имён его друзей — даже если друзей нет (тогда [], не null).

5.5 Агрегатные функции

Выполняются автоматически при группировке (по неагрегированным полям в RETURN или WITH).

ФункцияОсобенности
count(*)Считает строки (включая null).
count(expr)Считает не-null значения выражения. count(n)count(*), если n может быть null (например, в OPTIONAL MATCH).
sum(x), avg(x), min(x), max(x)Требуют числовых аргументов. При отсутствии строк — null, не 0.
collect(x)Возвращает список всех значений (включая null, если x может быть null). Порядок соответствует порядку строк в группе (но не гарантирован без ORDER BY).
percentileCont(p), percentileDisc(p)Непрерывный и дискретный перцентили. p ∈ [0.0, 1.0].
stDev(x), stDevP(x)Стандартное отклонение выборки и генеральной совокупности.

⚠️ Агрегация «съедает» переменные: после WITH u, count(r) AS n переменная r недоступна.

5.6 Временные функции

ФункцияПример
datetime(), localdatetime()Текущее время (с зоной / без). Неустойчивы.
duration.between(start, end)Разница между двумя датами/временами. Возвращает Duration.
date.truncate('month', d)Округление до начала месяца, квартала и т.п. Поддерживает: 'millennium', 'century', 'decade', 'year', 'quarter', 'month', 'week', 'day'.

Пример:

WITH datetime('2025-11-21T15:30:00+03:00') AS now
RETURN date.truncate('week', now) // → 2025-11-17T00:00:00+03:00

5.7 Пространственные функции

ФункцияОписание
point({x: …, y: …})2D декартова точка.
point({latitude: …, longitude: …})Географическая (WGS-84). Автоматически 2D/3D при height.
distance(p1, p2)Возвращает расстояние в метрах (географическая) или единицах координат (декартова). Работает только между точками одного типа.
point.withinBBox(point, min, max)Neo4j 5.21+, проверяет попадание в прямоугольник (bounding box).
point.withinCircle(center, radius)Нет стандартной функции — эмулируется через distance(p, center) <= radius.

5.8 Функции APOC (не встроенные, но широко используемые)

Модуль APOC (Awesome Procedures on Cypher) — не часть ядра, но стандарт де-факто. Основные полезные функции:

ФункцияНазначение
apoc.text.slug(str)Преобразует в URL-friendly строку ("Привет, мир!" → "privet-mir").
apoc.date.convertFormat(str, fromFormat, toFormat)Конвертация дат между форматами (Java SimpleDateFormat).
apoc.coll.sort(list), apoc.coll.sortNodes(list, 'prop')Сортировка списков и узлов.
apoc.map.fromPairs([['a',1],['b',2]])Строит map из списка пар.
apoc.create.uuid()Генерирует UUID v4 (возвращает строку).
apoc.cypher.run(query, params)Динамический запрос внутри запроса (опасно, но мощно).

⚠️ Использование APOC снижает переносимость запроса. Указывайте его явно как зависимость.


6. Параметры запросов

Cypher поддерживает параметризованные запросы — обязательная практика для безопасности и производительности.

Синтаксис

MATCH (n:User {name: $name})
WHERE n.age > $minAge
RETURN n

Параметры передаются отдельно (в драйверах — как map/dict):

{
"name": "Алексей",
"minAge": 18
}

Типы параметров

  • Поддерживаются все скалярные типы (включая Date, Point как объекты).
  • Поддерживаются списки и map’ы.
  • Нельзя параметризовать:
    • Имена меток (: $label — запрещено)
    • Типы рёбер (-[r:$type]-> — запрещено)
    • Имена свойств в литералах (n.$prop = … — запрещено; но можно n[$prop] = …)
    • Ключевые слова ($clause в RETURN и т.д.)

✅ Разрешено динамическое имя свойства:

MATCH (n)
WHERE n[$propName] = $value
RETURN n

7. Индексы и ограничения в контексте Cypher

Индексы и ограничения не влияют на синтаксис Cypher, но критически влияют на план выполнения.

Типы индексов (Neo4j 5+)

ТипСинтаксис созданияКогда используется
B-tree (по умолчанию)CREATE INDEX user_name FOR (n:User) ON (n.name)Равенство, диапазоны, сортировка.
Full-textCREATE FULLTEXT INDEX post_title_desc FOR (n:Post) ON EACH [n.title, n.description]Поиск по подстроке, CONTAINS, STARTS WITH (для текста), =~ (неоптимально).
Point (пространственный)CREATE POINT INDEX geo_loc FOR (n:Place) ON (n.location)distance(), WITHIN BBOX, фильтры по координатам.
Text (над TEXT-свойствами)Устаревший (до Neo4j 3.5), заменён full-text.

Ограничения

ТипСинтаксисЭффект
UNIQUECREATE CONSTRAINT user_email FOR (n:User) REQUIRE n.email IS UNIQUEЗапрещает дубликаты; автоматически создаёт уникальный B-tree индекс.
NODE KEYCREATE CONSTRAINT user_ext_id FOR (n:User) REQUIRE (n.system, n.external_id) IS NODE KEYСоставной уникальный ключ (несколько свойств).
PROPERTY EXISTENCECREATE CONSTRAINT user_name_req FOR (n:User) REQUIRE n.name IS NOT NULLЗапрещает null в свойстве (только в Enterprise Edition).

🔹 При MERGE (n:User {email: $e}) наличие UNIQUE на email гарантирует атомарность и предотвращает гонки.


8. Производительность: EXPLAIN, PROFILE, планы

8.1 Управление выполнением

  • EXPLAIN … — показывает предполагаемый план без выполнения.
  • PROFILE … — выполняет запрос и показывает фактический план + статистику (строки, итерации, время).

8.2 Ключевые операторы в плане

ОператорЗначение
NodeByLabelScanПолный проход по всем узлам метки (плохо без WHERE).
NodeIndexSeekПоиск по индексу (хорошо).
NodeIndexScanСканирование индекса (например, при CONTAINS без full-text). Медленнее Seek.
Expand(All)Обход рёбер. Сложность ~ степень узла × количество узлов.
VarLengthExpandПеременная длина — потенциально экспоненциален.
Projection, Filter, Sort, EagerСтандартные операции.
EagerКрасный флаг: заставляет буферизовать все данные → OOM при больших объёмах. Возникает при смешивании чтения и записи в одном запросе без WITH.

8.3 Анти-паттерны

Анти-паттернПроблемаРешение
MATCH (n) WHERE n.prop CONTAINS 'text' без full-textПолный скан + фильтрСоздать full-text индекс.
MATCH (a)-[*]->(b) без ограниченийЭкспоненциальный взрыв путейИспользовать shortestPath, ограничить длину, добавить WHERE на свойства.
MERGE (a:Label1 {id: x}) MERGE (b:Label2 {id: y}) MERGE (a)-[:R]->(b)Может создать a и b дважды (гонка)Сначала MERGE узлы отдельно, затем MERGE ребро. Или использовать apoc.merge.node.
WITH * перед агрегациейПередаёт ненужные переменные → рост памятиПеречислять только нужное.
Длинная цепочка MATCH → CREATE → MATCH → CREATEНет изоляции → EagerРазбивать на подзапросы через CALL { … }.

9. Совместимость с GQL (ISO/IEC 39075:2022)

Cypher стал основой для Graph Query Language (GQL) — первого стандарта запросов к графовым БД (утверждён ISO в 2022 г.).

Что перешло в GQL:

  • Синтаксис MATCH, WHERE, RETURN.
  • Паттерны с ()-->(), переменной длиной [*n..m].
  • Использование : для меток/типов.
  • WITH, UNWIND, CREATE, MERGE (с оговорками).
  • Типы данных (включая временные и пространственные).

Что не вошло или изменено:

  • REMOVE, DETACH DELETE — заменены на SET … = NULL и явное управление ссылками.
  • OPTIONAL MATCHLEFT MATCH (в GQL).
  • MERGE — нет в GQL; предлагается использовать комбинацию MATCH + INSERT.
  • Параметры — $name, но синтаксис может варьироваться.
  • Функции — только стандартный набор; apoc.* и id() исключены.

🔹 Neo4j 5.21+ частично поддерживает подмножество GQL (через флаг cypher.gql_compat=true). Memgraph и Oracle PGQL ближе к GQL.


10. Практические рекомендации для документации и преподавания

  1. Избегайте упоминания id() — это внутренняя деталь реализации.
  2. Всегда используйте параметры — даже в учебных примерах.
  3. Демонстрируйте EXPLAIN — чтобы учащиеся видели разницу между Scan и Seek.
  4. Покажите разницу COUNT(*) vs COUNT(n) — частая ошибка новичков.
  5. Объясняйте «жадность» MERGE на живых примерах (включая создание фантомных узлов).
  6. Вводите pattern comprehensions рано — они делают запросы компактнее и чище.

11. Грамматика Cypher (упрощённая EBNF)

Для целей документации «Вселенной IT» приводим формализованную, но читаемую структуру языка. Основана на Cypher Grammar (OpenCypher), адаптирована под Neo4j 5.x.

11.1 Базовые терминалы

Identifier        = [a-zA-Z_][a-zA-Z0-9_]* ;
SymbolicName = Identifier ; // имя переменной: n, rel, user
LabelName = ":" Identifier ; // :User, :Product
RelTypeName = ":" Identifier ; // :FRIENDS, :PURCHASED
PropertyKeyName = Identifier ; // name, created_at

StringLiteral = '"' ( ~["\\] | '\\' ["\\/bfnrt] | '\\' 'u' [0-9a-fA-F]{4} )* '"' ;
IntegerLiteral = [0-9]+ ;
FloatLiteral = [0-9]+ "." [0-9]* ( [eE] [+-]? [0-9]+ )? ;
BooleanLiteral = "true" | "false" ;
NullLiteral = "null" ;
Parameter = "$" Identifier ;

11.2 Узел и ребро

NodePattern       = "(" [SymbolicName] [LabelName {LabelName}] [Properties]? ")" ;
RelationshipPattern
= "-[" [SymbolicName] [RelTypes]? [Range]? [Properties]? "]-" ;
| "-[" [SymbolicName] [RelTypes]? [Range]? [Properties]? "]->" ;
| "<-[" [SymbolicName] [RelTypes]? [Range]? [Properties]? "]-" ;

RelTypes = RelTypeName { "|" RelTypeName } ;
Range = "*" [IntegerLiteral] [".." [IntegerLiteral]] ;
Properties = "{" [PropertyExpression {"," PropertyExpression}] "}" ;
PropertyExpression= PropertyKeyName ":" Expression ;

Пример соответствия:

(n:User:Admin {name: "Тимур", active: true})
-[r:AUTHORED|MODIFIED*1..3 {at: $ts}]→
(m:Article)

11.3 Паттерны и запрос

Pattern           = NodePattern { RelationshipPattern NodePattern } ;

Query = [UseClause]
{ ReadClause | WithClause | UnwindClause }
{ UpdateClause }
ReturnClause? ;

ReadClause = "MATCH" Pattern { "," Pattern }
| "OPTIONAL MATCH" Pattern { "," Pattern } ;

WithClause = "WITH" [Distinct]? ReturnBody ;
UnwindClause = "UNWIND" Expression "AS" SymbolicName ;

UpdateClause = CreateClause
| MergeClause
| SetClause
| RemoveClause
| DeleteClause ;

CreateClause = "CREATE" Pattern { "," Pattern } ;
MergeClause = "MERGE" Pattern
[ "ON CREATE" SetClause ]
[ "ON MATCH" SetClause ] ;

SetClause = "SET" ( SetItem { "," SetItem } ) ;
SetItem = SymbolicName "." PropertyKeyName "=" Expression
| SymbolicName "+=" MapLiteral
| SymbolicName LabelName {LabelName} ;

RemoveClause = "REMOVE" ( RemoveItem { "," RemoveItem } ) ;
RemoveItem = SymbolicName "." PropertyKeyName
| SymbolicName LabelName ;

DeleteClause = ("DELETE" | "DETACH DELETE") Expression { "," Expression } ;

ReturnClause = "RETURN" [Distinct]? ReturnBody ;
ReturnBody = Projection { "," Projection }
[ OrderBy? ]
[ Skip? ]
[ Limit? ] ;

Projection = Expression [ "AS" SymbolicName ] ;

Expression = OrExpression ;
OrExpression = XorExpression { "OR" XorExpression } ;
XorExpression = AndExpression { "XOR" AndExpression } ;
AndExpression = NotExpression { "AND" NotExpression } ;
NotExpression = "NOT" NotExpression | Comparison ;
Comparison = AddOrSubtract
[ ("=" | "<>" | "<" | "<=" | ">" | ">=" | "IS" ["NOT"] "NULL"
| "CONTAINS" | "STARTS WITH" | "ENDS WITH"
| "=~") AddOrSubtract ] ;

🔹 Полная грамматика содержит ~200 правил; здесь приведено ядро, достаточное для понимания структуры и автоматизированной проверки.


12. Сравнение с другими графовыми языками

КритерийCypherPGQL (Oracle)GremlinGQL (ISO)
ПарадигмаДекларативныйДекларативныйИмперативный/функциональныйДекларативный
СинтаксисASCII-art графыSQL-подобныйЦепочки вызовов (g.V().out()…)Основан на Cypher
MATCHMATCH (a)-[:KNOWS]->(b)SELECT a, b FROM MATCH (a)-[e:KNOWS]->(b)g.V().hasLabel('A').out('KNOWS').as('b')(a)-[:KNOWS]->(b)
Переменная длина[*1..3]{1,3}repeat(out()).times(3)[*1..3]
АгрегацияRETURN count(*)GROUP BY … SELECT COUNT(*)group().by(count())RETURN count(*)
ПодзапросыCALL { … }Подзапросы в FROMВложенные цепочки, sideEffect()Поддержка через LET и CALL
Параметры$param:param (bind variables)Переменные замыкания (в Groovy/Java)$param
ПроизводительностьОптимизируется до Pipe-плановCBO на основе статистик OracleJIT-компиляция в Traversal VMСтандартизированный CBO
Поддержка типовСлабая (динамическая схема)Интеграция с типами SQLНет типов (все — Object)Типизация через профили

🔸 Cypher vs Gremlin:

  • Cypher лучше для аналитических и поисковых задач (например, «найти друзей друзей»).
  • Gremlin эффективнее для процедурных и итеративных алгоритмов (PageRank, SSSP, LPA), особенно при глубокой вложенности.

🔸 PGQL ближе к SQL — легко осваивается DBA Oracle, но менее выразителен для сложных графовых топологий.


13. Сложные сценарии: примеры с пояснением

13.1 Рекомендательная система (Collaborative Filtering)

Задача: «Найти товары, купленные друзьями пользователя, но не купленные им самим».

MATCH (u:User {id: $user_id})
MATCH (u)-[:FRIENDS*1..2]-(f:User)-[:BOUGHT]->(p:Product)
WHERE NOT (u)-[:BOUGHT]->(p)
WITH p, count(*) AS score
ORDER BY score DESC
LIMIT 10
RETURN p.name, p.category, score

⚠️ Проблемы:

  • FRIENDS*1..2 может создать дубликаты (если два пути до одного друга).
    Решение: WITH DISTINCT f после первого MATCH.
  • NOT (u)-[:BOUGHT]->(p) — может быть медленным без индекса на (u)-[:BOUGHT]->(p).
    Решение: Предварительно собрать COLLECT(id(p)) купленных товаров → фильтровать через WHERE NOT id(p) IN bought_ids.

13.2 Обнаружение циклов (например, саморекурсивные зависимости)

MATCH p = (n)-[:DEPENDS_ON*2..]->(n)
WHERE length(p) = size(nodes(p)) // исключить повторяющиеся узлы (простой цикл)
RETURN nodes(p) AS cycle, length(p) AS depth
LIMIT 5

🔹 *2.. — минимальная длина цикла = 2 (ребро → узел → обратно).
🔹 Условие length(p) = size(nodes(p)) гарантирует, что все узлы уникальны (простой цикл).
❗ Без LIMIT и ограничения длины запрос может не завершиться.

Альтернатива — использование shortestPath для проверки существования цикла:

MATCH (n)
WHERE shortestPath((n)-[:DEPENDS_ON*1..]->(n)) IS NOT NULL
RETURN n
LIMIT 10

13.3 Выявление сообществ (Community Detection через Label Propagation — упрощённо)

Neo4j Graph Data Science Library (GDS) рекомендуется для production, но можно эмулировать:

// Шаг 1: инициализация — каждому узлу — свой "label"
MATCH (n:User)
SET n.temp_label = id(n)

// Шаг 2: итеративно обновлять метку → мода соседей
// (выполняется N раз вручную или через APOC)
CALL apoc.periodic.iterate(
"MATCH (n:User) RETURN n",
"MATCH (n)-[:FRIENDS]-(f)
WITH n, apoc.coll.frequencies([f IN collect(f) | f.temp_label]) AS freqs
WITH n, reduce(m = {label: null, count: 0}, item IN freqs |
CASE WHEN item.count > m.count THEN {label: item.item, count: item.count} ELSE m END
).label AS new_label
SET n.temp_label = new_label",
{batchSize: 1000, parallel: true}
)
YIELD batches, total
RETURN batches, total

🔸 Это учебная реализация. В реальности — используйте gds.labelPropagation.stream().


14. Подзапросы и композиция

14.1 CALL { … } — изолированный подзапрос

MATCH (u:User {active: true})
CALL {
WITH u
MATCH (u)-[:POSTED]->(p:Post)
WHERE p.created > datetime() - duration('P7D')
RETURN count(p) AS recent_posts
}
RETURN u.name, recent_posts

Особенности:

  • WITH u — передача контекста.
  • Переменные извне (u) доступны только в первом WITH подзапроса.
  • Переменные внутри (recent_posts) доступны снаружи.
  • Подзапрос выполняется на каждой строке внешнего потока.

14.2 UNION и UNION ALL

MATCH (u:User)-[:MODERATOR_OF]->(g:Group)
RETURN u.name AS principal, g.name AS target, 'moderator' AS role
UNION
MATCH (u:User)-[:MEMBER_OF]->(g:Group)
RETURN u.name, g.name, 'member'
  • UNION — удаляет дубликаты (требует совпадения имён/типов колонок).
  • UNION ALL — сохраняет дубликаты, быстрее.

❗ Нельзя использовать UNION между READ и WRITE — только в чисто-RETURN-части.


15. Особенности реализаций

15.1 Neo4j 5.21+ (Enterprise/Community)

  • Полная поддержка GQL подмножества (через cypher.gql_compat=true).
  • Composite indexes — индексы по нескольким свойствам:
    CREATE INDEX user_status_created FOR (n:User) ON (n.status, n.created_at)
    Используется при WHERE n.status = 'active' AND n.created_at > … (в порядке объявления).
  • Subquery predicates:
    WHERE EXISTS {
    MATCH (n)-[:FRIENDS]->(f:User)
    WHERE f.trust_score > 0.8
    }
    Замена устаревшему exists((n)-[:FRIENDS]->(:User {trust_score: …})).

15.2 Apache AGE (PostgreSQL extension)

  • Реализует ~80% Cypher-синтаксиса.
  • Нет поддержки:
    • MERGE (только CREATE + ручная проверка),
    • REMOVE,
    • полных временных типов (datetimetimestamptz),
    • apoc.*.
  • Работает поверх PostgreSQL, использует его индексы (GIN, GiST) и параллелизм.
  • Пример запроса:
    SELECT * FROM cypher('graph_name', $$
    MATCH (u:User {status: 'active'})
    RETURN u.name, u.props->>'email'
    $$) AS (name agtype, email agtype);

🔹 AGE — хороший выбор, если уже есть PostgreSQL и нужна гибридная (реляционная + графовая) модель.